{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b323f441-93b8-4b61-b874-22d099a4df07",
   "metadata": {},
   "source": [
    "# OpenAI Whisper LoRA 模型评估和推理\n",
    "\n",
    "本教程将加载 LoRA 微调后的 Whisper 模型进行模型评估（基于 WER 指标），并将数据处理和模型准备流程也整合进来。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "1b5a62f2-1079-4f06-8afc-9135db361d4d",
   "metadata": {},
   "outputs": [],
   "source": [
    "model_name_or_path = \"openai/whisper-large-v2\"\n",
    "model_dir = \"models/whisper-large-v2-asr-int8\"\n",
    "\n",
    "language = \"Chinese (China)\"\n",
    "language_abbr = \"zh-CN\"\n",
    "task = \"transcribe\"\n",
    "dataset_name = \"mozilla-foundation/common_voice_11_0\"\n",
    "\n",
    "batch_size=16"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "6c638814-d20b-4af9-b9c4-09f76ba1b631",
   "metadata": {
    "collapsed": true,
    "jupyter": {
     "outputs_hidden": true
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "WhisperForConditionalGeneration(\n",
       "  (model): WhisperModel(\n",
       "    (encoder): WhisperEncoder(\n",
       "      (conv1): Conv1d(80, 1280, kernel_size=(3,), stride=(1,), padding=(1,))\n",
       "      (conv2): Conv1d(1280, 1280, kernel_size=(3,), stride=(2,), padding=(1,))\n",
       "      (embed_positions): Embedding(1500, 1280)\n",
       "      (layers): ModuleList(\n",
       "        (0-31): 32 x WhisperEncoderLayer(\n",
       "          (self_attn): WhisperSdpaAttention(\n",
       "            (k_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=False)\n",
       "            (v_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "            (q_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "            (out_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "          )\n",
       "          (self_attn_layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "          (activation_fn): GELUActivation()\n",
       "          (fc1): Linear8bitLt(in_features=1280, out_features=5120, bias=True)\n",
       "          (fc2): Linear8bitLt(in_features=5120, out_features=1280, bias=True)\n",
       "          (final_layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "        )\n",
       "      )\n",
       "      (layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "    )\n",
       "    (decoder): WhisperDecoder(\n",
       "      (embed_tokens): Embedding(51865, 1280, padding_idx=50257)\n",
       "      (embed_positions): WhisperPositionalEmbedding(448, 1280)\n",
       "      (layers): ModuleList(\n",
       "        (0-31): 32 x WhisperDecoderLayer(\n",
       "          (self_attn): WhisperSdpaAttention(\n",
       "            (k_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=False)\n",
       "            (v_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "            (q_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "            (out_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "          )\n",
       "          (activation_fn): GELUActivation()\n",
       "          (self_attn_layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "          (encoder_attn): WhisperSdpaAttention(\n",
       "            (k_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=False)\n",
       "            (v_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "            (q_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "            (out_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "          )\n",
       "          (encoder_attn_layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "          (fc1): Linear8bitLt(in_features=1280, out_features=5120, bias=True)\n",
       "          (fc2): Linear8bitLt(in_features=5120, out_features=1280, bias=True)\n",
       "          (final_layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "        )\n",
       "      )\n",
       "      (layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "    )\n",
       "  )\n",
       "  (proj_out): Linear(in_features=1280, out_features=51865, bias=False)\n",
       ")"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from transformers import AutoModelForSpeechSeq2Seq, AutoTokenizer, AutoProcessor\n",
    "from peft import PeftConfig, PeftModel\n",
    "\n",
    "peft_config = PeftConfig.from_pretrained(model_dir)\n",
    "\n",
    "base_model = AutoModelForSpeechSeq2Seq.from_pretrained(\n",
    "    peft_config.base_model_name_or_path, load_in_8bit=True, device_map=\"auto\"\n",
    ")\n",
    "base_model.requires_grad_(False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "24a1d9e4-e243-46e4-bff5-8d51ab302d21",
   "metadata": {
    "collapsed": true,
    "jupyter": {
     "outputs_hidden": true
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "PeftModel(\n",
       "  (base_model): LoraModel(\n",
       "    (model): WhisperForConditionalGeneration(\n",
       "      (model): WhisperModel(\n",
       "        (encoder): WhisperEncoder(\n",
       "          (conv1): Conv1d(80, 1280, kernel_size=(3,), stride=(1,), padding=(1,))\n",
       "          (conv2): Conv1d(1280, 1280, kernel_size=(3,), stride=(2,), padding=(1,))\n",
       "          (embed_positions): Embedding(1500, 1280)\n",
       "          (layers): ModuleList(\n",
       "            (0-31): 32 x WhisperEncoderLayer(\n",
       "              (self_attn): WhisperSdpaAttention(\n",
       "                (k_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=False)\n",
       "                (v_proj): lora.Linear8bitLt(\n",
       "                  (base_layer): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "                  (lora_dropout): ModuleDict(\n",
       "                    (default): Dropout(p=0.05, inplace=False)\n",
       "                  )\n",
       "                  (lora_A): ModuleDict(\n",
       "                    (default): Linear(in_features=1280, out_features=4, bias=False)\n",
       "                  )\n",
       "                  (lora_B): ModuleDict(\n",
       "                    (default): Linear(in_features=4, out_features=1280, bias=False)\n",
       "                  )\n",
       "                  (lora_embedding_A): ParameterDict()\n",
       "                  (lora_embedding_B): ParameterDict()\n",
       "                )\n",
       "                (q_proj): lora.Linear8bitLt(\n",
       "                  (base_layer): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "                  (lora_dropout): ModuleDict(\n",
       "                    (default): Dropout(p=0.05, inplace=False)\n",
       "                  )\n",
       "                  (lora_A): ModuleDict(\n",
       "                    (default): Linear(in_features=1280, out_features=4, bias=False)\n",
       "                  )\n",
       "                  (lora_B): ModuleDict(\n",
       "                    (default): Linear(in_features=4, out_features=1280, bias=False)\n",
       "                  )\n",
       "                  (lora_embedding_A): ParameterDict()\n",
       "                  (lora_embedding_B): ParameterDict()\n",
       "                )\n",
       "                (out_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "              )\n",
       "              (self_attn_layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "              (activation_fn): GELUActivation()\n",
       "              (fc1): Linear8bitLt(in_features=1280, out_features=5120, bias=True)\n",
       "              (fc2): Linear8bitLt(in_features=5120, out_features=1280, bias=True)\n",
       "              (final_layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "            )\n",
       "          )\n",
       "          (layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "        )\n",
       "        (decoder): WhisperDecoder(\n",
       "          (embed_tokens): Embedding(51865, 1280, padding_idx=50257)\n",
       "          (embed_positions): WhisperPositionalEmbedding(448, 1280)\n",
       "          (layers): ModuleList(\n",
       "            (0-31): 32 x WhisperDecoderLayer(\n",
       "              (self_attn): WhisperSdpaAttention(\n",
       "                (k_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=False)\n",
       "                (v_proj): lora.Linear8bitLt(\n",
       "                  (base_layer): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "                  (lora_dropout): ModuleDict(\n",
       "                    (default): Dropout(p=0.05, inplace=False)\n",
       "                  )\n",
       "                  (lora_A): ModuleDict(\n",
       "                    (default): Linear(in_features=1280, out_features=4, bias=False)\n",
       "                  )\n",
       "                  (lora_B): ModuleDict(\n",
       "                    (default): Linear(in_features=4, out_features=1280, bias=False)\n",
       "                  )\n",
       "                  (lora_embedding_A): ParameterDict()\n",
       "                  (lora_embedding_B): ParameterDict()\n",
       "                )\n",
       "                (q_proj): lora.Linear8bitLt(\n",
       "                  (base_layer): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "                  (lora_dropout): ModuleDict(\n",
       "                    (default): Dropout(p=0.05, inplace=False)\n",
       "                  )\n",
       "                  (lora_A): ModuleDict(\n",
       "                    (default): Linear(in_features=1280, out_features=4, bias=False)\n",
       "                  )\n",
       "                  (lora_B): ModuleDict(\n",
       "                    (default): Linear(in_features=4, out_features=1280, bias=False)\n",
       "                  )\n",
       "                  (lora_embedding_A): ParameterDict()\n",
       "                  (lora_embedding_B): ParameterDict()\n",
       "                )\n",
       "                (out_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "              )\n",
       "              (activation_fn): GELUActivation()\n",
       "              (self_attn_layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "              (encoder_attn): WhisperSdpaAttention(\n",
       "                (k_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=False)\n",
       "                (v_proj): lora.Linear8bitLt(\n",
       "                  (base_layer): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "                  (lora_dropout): ModuleDict(\n",
       "                    (default): Dropout(p=0.05, inplace=False)\n",
       "                  )\n",
       "                  (lora_A): ModuleDict(\n",
       "                    (default): Linear(in_features=1280, out_features=4, bias=False)\n",
       "                  )\n",
       "                  (lora_B): ModuleDict(\n",
       "                    (default): Linear(in_features=4, out_features=1280, bias=False)\n",
       "                  )\n",
       "                  (lora_embedding_A): ParameterDict()\n",
       "                  (lora_embedding_B): ParameterDict()\n",
       "                )\n",
       "                (q_proj): lora.Linear8bitLt(\n",
       "                  (base_layer): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "                  (lora_dropout): ModuleDict(\n",
       "                    (default): Dropout(p=0.05, inplace=False)\n",
       "                  )\n",
       "                  (lora_A): ModuleDict(\n",
       "                    (default): Linear(in_features=1280, out_features=4, bias=False)\n",
       "                  )\n",
       "                  (lora_B): ModuleDict(\n",
       "                    (default): Linear(in_features=4, out_features=1280, bias=False)\n",
       "                  )\n",
       "                  (lora_embedding_A): ParameterDict()\n",
       "                  (lora_embedding_B): ParameterDict()\n",
       "                )\n",
       "                (out_proj): Linear8bitLt(in_features=1280, out_features=1280, bias=True)\n",
       "              )\n",
       "              (encoder_attn_layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "              (fc1): Linear8bitLt(in_features=1280, out_features=5120, bias=True)\n",
       "              (fc2): Linear8bitLt(in_features=5120, out_features=1280, bias=True)\n",
       "              (final_layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "            )\n",
       "          )\n",
       "          (layer_norm): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)\n",
       "        )\n",
       "      )\n",
       "      (proj_out): Linear(in_features=1280, out_features=51865, bias=False)\n",
       "    )\n",
       "  )\n",
       ")"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "peft_model = PeftModel.from_pretrained(base_model, model_dir)\n",
    "peft_model.eval()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "cdb02d5f-07f5-4ef1-87cb-0c81925bf586",
   "metadata": {},
   "outputs": [],
   "source": [
    "tokenizer = AutoTokenizer.from_pretrained(peft_config.base_model_name_or_path, language=language, task=task)\n",
    "processor = AutoProcessor.from_pretrained(peft_config.base_model_name_or_path, language=language, task=task)\n",
    "feature_extractor = processor.feature_extractor"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec285096-52a5-4e7a-9520-451c1b17c349",
   "metadata": {},
   "source": [
    "## 评估数据集处理"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "9646f874-182c-4a6b-b000-b35f90fc893e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from datasets import load_dataset, DatasetDict, Audio\n",
    "\n",
    "common_voice = DatasetDict()\n",
    "common_voice[\"test\"] = load_dataset(dataset_name, language_abbr, split=\"test\", trust_remote_code=True)\n",
    "common_voice = common_voice.remove_columns(\n",
    "    [\"accent\", \"age\", \"client_id\", \"down_votes\", \"gender\", \"locale\", \"path\", \"segment\", \"up_votes\"]\n",
    ")\n",
    "common_voice = common_voice.cast_column(\"audio\", Audio(sampling_rate=16000))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "2a0f0d3f-71d5-4cae-8230-70b1fb4548d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "def prepare_dataset(batch):\n",
    "    audio = batch[\"audio\"]\n",
    "    batch[\"input_features\"] = feature_extractor(audio[\"array\"], sampling_rate=audio[\"sampling_rate\"]).input_features[0]\n",
    "    batch[\"labels\"] = tokenizer(batch[\"sentence\"]).input_ids\n",
    "    return batch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "be3d5a3c-d498-4250-a590-ccd51e9b03f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "small_common_voice = DatasetDict()\n",
    "\n",
    "small_common_voice[\"test\"] = common_voice[\"test\"].shuffle(seed=16).select(range(320))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "7c4e47ac-2001-47c0-a53a-9490c94bc784",
   "metadata": {},
   "outputs": [],
   "source": [
    "tokenized_common_voice = small_common_voice.map(prepare_dataset)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b3875e35-9c9f-47d6-be11-ee553723179d",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "f2aa0d9e-8ca7-47ed-8856-cebce1d93c65",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "\n",
    "from dataclasses import dataclass\n",
    "from typing import Any, Dict, List, Union\n",
    "\n",
    "# 定义一个针对语音到文本任务的数据整理器类\n",
    "@dataclass\n",
    "class DataCollatorSpeechSeq2SeqWithPadding:\n",
    "    processor: Any  # 处理器结合了特征提取器和分词器\n",
    "\n",
    "    # 整理器函数，将特征列表处理成一个批次\n",
    "    def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:\n",
    "        # 从特征列表中提取输入特征，并填充以使它们具有相同的形状\n",
    "        input_features = [{\"input_features\": feature[\"input_features\"]} for feature in features]\n",
    "        batch = self.processor.feature_extractor.pad(input_features, return_tensors=\"pt\")\n",
    "\n",
    "        # 从特征列表中提取标签特征（文本令牌），并进行填充\n",
    "        label_features = [{\"input_ids\": feature[\"labels\"]} for feature in features]\n",
    "        labels_batch = self.processor.tokenizer.pad(label_features, return_tensors=\"pt\")\n",
    "\n",
    "        # 使用-100替换标签中的填充区域，-100通常用于在损失计算中忽略填充令牌\n",
    "        labels = labels_batch[\"input_ids\"].masked_fill(labels_batch.attention_mask.ne(1), -100)\n",
    "\n",
    "        # 如果批次中的所有序列都以句子开始令牌开头，则移除它\n",
    "        if (labels[:, 0] == self.processor.tokenizer.bos_token_id).all().cpu().item():\n",
    "            labels = labels[:, 1:]\n",
    "\n",
    "        # 将处理过的标签添加到批次中\n",
    "        batch[\"labels\"] = labels\n",
    "\n",
    "        return batch  # 返回最终的批次，准备好进行训练或评估\n",
    "\n",
    "# 用给定的处理器实例化数据整理器\n",
    "data_collator = DataCollatorSpeechSeq2SeqWithPadding(processor=processor)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f7eeb613-1dfb-45f0-828d-36150ed5b335",
   "metadata": {},
   "source": [
    "## 评估模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "3e774b45-69a9-4f95-938d-e3284ff6060f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import evaluate\n",
    "\n",
    "# 词错误率（WER）是评估ASR模型常用的指标。从 Evaluate 加载 WER 指标\n",
    "metric = evaluate.load(\"wer\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "6e5c0c9d-5ccd-4770-9cfc-78e51c956baa",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch.utils.data import DataLoader\n",
    "from tqdm import tqdm\n",
    "import numpy as np\n",
    "import gc\n",
    "\n",
    "eval_dataloader = DataLoader(tokenized_common_voice[\"test\"], batch_size=batch_size, collate_fn=data_collator)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "e1371372-b803-4cb0-96a2-110df3d52541",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "  0%|          | 0/20 [00:00<?, ?it/s]/root/miniconda3/lib/python3.11/site-packages/bitsandbytes/autograd/_functions.py:322: UserWarning: MatMul8bitLt: inputs will be cast from torch.float32 to float16 during quantization\n",
      "  warnings.warn(f\"MatMul8bitLt: inputs will be cast from {A.dtype} to float16 during quantization\")\n",
      "100%|██████████| 20/20 [05:25<00:00, 16.26s/it]\n"
     ]
    }
   ],
   "source": [
    "# 遍历评估数据加载器中的所有批次\n",
    "for step, batch in enumerate(tqdm(eval_dataloader)):\n",
    "    # 使用自动混合精度来加速计算，并减少显存使用\n",
    "    with torch.cuda.amp.autocast():\n",
    "        # 不计算梯度，以节省计算资源，仅用于推理和评估\n",
    "        with torch.no_grad():\n",
    "            # 生成预测的标记(tokens)，这里使用模型的generate函数进行文本生成\n",
    "            generated_tokens = (\n",
    "                peft_model.generate(\n",
    "                    input_features=batch[\"input_features\"].to(\"cuda\"),  # 将输入特征移动到GPU上\n",
    "                    decoder_input_ids=batch[\"labels\"][:, :4].to(\"cuda\"),  # 提供解码器的初始输入\n",
    "                    max_new_tokens=255,  # 设置生成的最大新标记数量\n",
    "                )\n",
    "                .cpu()  # 将生成的标记移回CPU\n",
    "                .numpy()  # 转换为NumPy数组以便进一步处理\n",
    "            )\n",
    "            # 获取批次中的标签，并将其移回CPU\n",
    "            labels = batch[\"labels\"].cpu().numpy()\n",
    "            # 将标签中的-100替换为填充标记的ID，-100通常用于忽略计算损失的标记\n",
    "            labels = np.where(labels != -100, labels, tokenizer.pad_token_id)\n",
    "            # 使用分词器解码生成的标记和标签，以获得可读的文本\n",
    "            decoded_preds = tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)\n",
    "            decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)\n",
    "            # 将预测和参考添加到评估指标中，用于后续的性能评估\n",
    "            metric.add_batch(\n",
    "                predictions=decoded_preds,\n",
    "                references=decoded_labels,\n",
    "            )\n",
    "    # 删除不再需要的变量以释放内存\n",
    "    del generated_tokens, labels, batch\n",
    "    # 手动触发垃圾收集，进一步清理内存\n",
    "    gc.collect()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41525e3b-3fe5-45cc-9e5c-32033960f424",
   "metadata": {},
   "source": [
    "### 使用全量数据微调后，对比 WER 指标降低了多少"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "d2276bf1-1ab3-453c-9c7a-93486b829332",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wer=70.0%\n"
     ]
    }
   ],
   "source": [
    "# 计算词错误率（WER）指标，并将结果转换为百分比形式\n",
    "wer = 100 * metric.compute()\n",
    "\n",
    "# 打印词错误率，f\"{wer=}\"是一种格式化字符串的简洁写法，它会展示变量名和值\n",
    "print(f\"{wer=}%\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e698dc9f-e54d-4faf-a7a4-db5cc7ce6d3f",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
